goog.require('dkpoints.auth.Auth');
goog.require('dkpoints.tests.stubFn');

goog.require('goog.testing.jsunit');

var auth, rpc, menu, statusPanel = null, stubFn;

function setUpPage() {
  stubFn = dkpoints.tests.stubFn;
}

var setUp = function() {
  rpc = {
    execute: stubFn()
  };

  menu = {
    setAuthStatus: stubFn()
  };

  statusPanel = {
    addMessage: stubFn()
  };

  auth = new dkpoints.auth.Auth(rpc, menu, statusPanel);
};


/**
 * Ensure that, once initialized, the auth object is of the right class.
 * Also implies that the constructor did not throw an exception.
 */
var testAuthClass = function() {
  assertTrue('auth should be an instance of dkpoints.auth.Auth',
             auth instanceof dkpoints.auth.Auth);
  assertEquals('Constructor should have stored reference to RPC service.',
               rpc, auth.rpc_);
  assertEquals('Constructor should have stored reference to menu object.',
               menu, auth.menu_);
  assertEquals('Constructor should have stored reference to Status manager.',
               statusPanel, auth.status_);
};


/**
 * Ensure that, on initialization, the auth manager initiates a checkStatus
 * call.
 */
var testAuthInitRPCCall = function() {
  auth.checkStatus = stubFn();
  auth.init();
  assertTrue('Auth.checkStatus should have been called.',
             auth.checkStatus.called);
};


/**
 * Test that setNavManager stores a reference to the given nav manager.
 */
var testSetNavManagerSetsNavManager = function() {
  var nav = {};
  auth.setNavManager(nav);
  assertEquals('Auth.navManager_ should be the object we gave it.',
               nav, auth.navManager_);
};


/**
 * Test that checkStatus doesn't add null/undefined callbacks to
 * listenerCallbacks_ but does add actual listeners.
 */
var testCheckStatusProperlyStoresListeners = function() {
  // Lets us skip all the other stuff in the method.
  auth.waitingForResponse_ = true;

  assertTrue('Auth.listenerCallbacks_ should default to zero-length array.',
             auth.listenerCallbacks_.length === 0);

  auth.checkStatus();
  assertTrue('checkStatus should not have added undefined callback to ' +
             'listenerCallbacks_.', auth.listenerCallbacks_.length === 0);

  auth.checkStatus(null);
  assertTrue('checkStatus should not have added null callback to ' +
             'listenerCallbacks_.', auth.listenerCallbacks_.length === 0);

  var callback1 = stubFn(1),
      callback2 = stubFn(2);

  auth.checkStatus(callback1);
  assertEquals('checkStatus should have added callback to listenerCallbacks_.',
               auth.listenerCallbacks_[0], callback1);

  auth.checkStatus(callback2);
  assertEquals('checkStatus should have added callback to listenerCallbacks_',
               auth.listenerCallbacks_[1], callback2);
};


/**
 * Make sure the argument to rpc.execute is an AuthCheckCommand.
 */
var testCheckStatusRPCExecuteArgumentClass = function() {
  auth.checkStatus(goog.nullFunction);
  assertTrue('The argument to rpc.execute should be an AuthCheckCommand',
             rpc.execute.args[0] instanceof
             dkpoints.rpc.commands.AuthCheckCommand);
  assertTrue('Auth.waitingForResponse_ should have been set to true on ' +
             'rpc.execute.', auth.waitingForResponse_);
};


/**
 * Test that multiple simultaneous calls to checkStatus only result in a single
 * RPC call.
 */
var testMultiRequestSingleRPC = function() {
  var rpcRequestsMade = 0;
  rpc.execute = function() { rpcRequestsMade++ };
  auth.checkStatus();
  auth.checkStatus();

  assertTrue('rpc.execute should only have been called once',
             rpcRequestsMade === 1);
};


/**
 * On successful checkStatus return, notifyListeners should be called with the
 * given authStatus object.
 */
var testOnCheckSuccessNotifyListeners = function() {
  auth.setAuthStatus_ = goog.nullFunction;
  var notify = auth.notifyListeners_ = stubFn(),
      authStatus = {
        params: 'Moo'
      };

  auth.onCheckSuccess(authStatus);
  assertTrue('NotifyListeners should have been executed onCheckSuccess.',
             auth.notifyListeners_.called);
  assertEquals('onCheckSuccess should have passed notifyListeners the ' +
               'authStatus object.', authStatus, notify.args[0]);
};


/**
 * Once a failed response is returned, notifyListeners should be called with
 * a default (no-auth) authStatus object.
 */
var testOnCheckFailureNotifyListeners = function() {
  auth.setAuthStatus_ = goog.nullFunction;
  var notify = auth.notifyListeners_ = stubFn();

  auth.onCheckFailure(/* mock XHR event */);
  assertTrue('NotifyListeners should have been executed onCheckSuccess.',
             auth.notifyListeners_.called);
  var returnedAuth = notify.args[0],
      proper =
          returnedAuth instanceof dkpoints.rpc.commands.AuthCheckResponse &&
          returnedAuth.loggedIn === false &&
          returnedAuth.character === '' &&
          returnedAuth.isAdmin === false;

  assertTrue('onCheckFailure should have passed notifyListeners a default ' +
             'AuthCheckResponse objects.', proper);
};


/**
 * Test that notifyListeners_ calls auth.setAuthStatus_ and clears the
 * waitingForResponse flag before calling the first listener.
 */
var testNotifyListenersCallsSetAuthStatus = function() {
  var mockStatus = {},
      isWaiting = auth.waitingForResponse_ = true,
      callback = function() { isWaiting = auth.waitingForResponse_; };
  auth.setAuthStatus_ = stubFn();
  auth.listenerCallbacks_.push(callback);

  auth.notifyListeners_(mockStatus);
  assertTrue('NotifyListeners should have called setAuthStatus.',
             auth.setAuthStatus_.called);
  assertEquals('NotifyListeners should have passed setAuthStatus the given ' +
               'authStatus object.', mockStatus, auth.setAuthStatus_.args[0]);
  assertFalse('NotifyListeners should have cleared waitingForResponse ' +
              'before executing its callbacks.', isWaiting);
};


/**
 * Test that all callbacks in auth.listenerCallbacks_ are executed during
 * notifyListeners execution.
 */
var testNotifyListenersExecutesAllListeners = function() {
  auth.setAuthStatus_ = goog.nullFunction;

  var callback1 = stubFn(),
      callback2 = stubFn(),
      callback3 = stubFn(),
      authStatus = {};

  auth.listenerCallbacks_.push(callback1);
  auth.listenerCallbacks_.push(callback2);
  auth.listenerCallbacks_.push(callback3);

  auth.notifyListeners_(authStatus);

  assertTrue('Callback1 should have been executed', callback1.called);
  assertEquals('Callback1 should have been given the authStatus object.',
               authStatus, callback1.args[0]);
  assertTrue('Callback2 should have been executed', callback2.called);
  assertEquals('Callback2 should have been given the authStatus object.',
               authStatus, callback2.args[0]);
  assertTrue('Callback3 should have been executed', callback3.called);
  assertEquals('Callback3 should have been given the authStatus object.',
               authStatus, callback3.args[0]);
  assertEquals('Auth.listenerCallbacks_ should\'ve been set to an empty array.',
               0, auth.listenerCallbacks_.length);
};


/**
 * Test that the auth status object returned by getAuthStatus is the same one
 * that was received by onCheckSuccess.
 */
var testGetAuthStatusSync = function() {
  // Stub out for now to be tested separately.
  auth.menu_.setAuthStatus = goog.nullFunction;

  var defaultStatus = new dkpoints.rpc.commands.AuthCheckResponse({}),
      callback = stubFn();

  auth.authStatus_ = defaultStatus;
  auth.getAuthStatus(callback);

  assertEquals('getAuthStatus should have returned auth.authStatus_',
               defaultStatus, callback.args[0]);
};


/**
 * Test that when getAuthStatus is called during a request, that the callback
 * is eventually called with the same authStatus object that is received.
 */
var testGetAuthStatusAsync = function() {
  // Stub out for now to be tested separately.
  auth.menu_.setAuthStatus = goog.nullFunction;

  var callback = stubFn();

  auth.waitingForResponse_ = true;
  auth.getAuthStatus(callback);

  assertEquals('getAuthStatus should have put our listener in ' +
               'listenerCallbacks_', callback, auth.listenerCallbacks_[0]);
};


/**
 * Test that setAuthStatus properly stores a reference to the returned auth
 * status and calls the menu manager's setAuthStatus method.
 */
var testSetAuthStatus = function() {
  var authStatus = {},
      nav = auth.nav_ = {
        setAuthStatus: stubFn()
      };

  auth.setAuthStatus_(authStatus);

  assertEquals('Auth.authStatus_ should have been set to the one we gave it.',
               authStatus, auth.authStatus_);
  assertTrue('auth.menu_.setAuthStatus should have been called.',
             menu.setAuthStatus.called);
  assertTrue('auth.navManager_.setAuthStatus should have been called.',
             nav.setAuthStatus.called);
};


/**
 * Referer must be properly appended to the redirect URL when
 * referer !== '/auth/login'.
 */
var testLoginRedirect = function() {
  var baseURL = 'http://dkpoints.appspot.com',
      path = '/apis/auth/login',
      referer = '/tests/auth',
      expectedPath = baseURL + path + referer,
      realSetLocationHref = dkpoints.utils.setLocationHref,
      setHref = dkpoints.utils.setLocationHref = stubFn();

  auth.login(baseURL, referer);
  assertEquals('auth.login should redirect to: ' + expectedPath,
               expectedPath, setHref.args[0]);

  dkpoints.utils.setLocationHref = realSetLocationHref;
};


/**
 * Referer should be blank when referer === '/auth/login/'
 */
var testLoginRedirectWithLoginReferer = function() {
  var baseURL = 'http://dkpoints.appspot.com',
      path = '/apis/auth/login',
      referer = '/auth/login/',
      expectedPath = baseURL + path,
      realSetLocationHref = dkpoints.utils.setLocationHref,
      setHref = dkpoints.utils.setLocationHref = stubFn();

  auth.login(baseURL, referer);
  assertEquals('auth.login should redirect to: ' + expectedPath,
               expectedPath, setHref.args[0]);

  dkpoints.utils.setLocationHref = realSetLocationHref;
};


/**
 * Test that logout properly stores multiple listeners.
 */
var testLogoutProperlyStoresMultipleListeners = function() {
  var listeners = auth.listenerCallbacks_;
  assertEquals('Auth.listenerCallbacks_ should default to an empty array.',
               0, listeners.length);

  auth.logout(null);
  assertEquals('Auth.logout shouldn not have added a null listener.',
               0, auth.listenerCallbacks_.length);
  assertTrue('Calling auth.logout should set auth.waitingForResponse to true.',
             auth.waitingForResponse_);
  assertEquals('rpc.execute should have been called once.',
               1, rpc.execute.timesCalled);
  assertTrue('The argument to rpc.execute should have been a LogoutCommand.',
             rpc.execute.args[0] instanceof
                 dkpoints.rpc.commands.LogoutCommand);

  var callback1 = stubFn();
  auth.logout(callback1);
  assertEquals('Auth.logout should have stored the callback.',
               callback1, listeners[0]);
  assertEquals('rpc.execute should not have been called again.',
               1, rpc.execute.timesCalled);
};


/**
 * Test that logoutSuccess calls notifyListeners_ to update everyone of the
 * auth change.
 */
var testLogoutSuccessCallsNotifyListeners = function() {
  var authStatus = {};
  auth.notifyListeners_ = stubFn();

  auth.logoutSuccess_(authStatus);
  assertEquals('notifyListeners_ should have been called with the given ' +
               'authStatus object.', authStatus, auth.notifyListeners_.args[0]);
};


/**
 * Test that logoutFailure forces a server trip to verify the auth status.
 */
var testLogoutFailureForcesCheckStatus = function() {
  var waitingForResponse = null;
  auth.checkStatus = function() {
    auth.checkStatus.called = true;
    auth.checkStatus.args = arguments;
    waitingForResponse = auth.waitingForResponse_;
  };

  auth.logoutFailure_();
  assertTrue('Auth.logoutFailure_ should have called checkStatus.',
             auth.checkStatus.called);
  assertUndefined('logoutFailure_ should not pass a callback to checkStatus.',
                  auth.checkStatus.args[0]);
  assertFalse('logoutFailure must reset waitingForResponse so checkStatus ' +
              'make a new XHR call.', waitingForResponse);
};


/*
 * Mini integration tests.
 */


/**
 * I ran into a bug where a callback handed to getAuthStatus during a request
 * transitively called getAuthStatus again DURING the notifyListeners_ loop.
 * This caused a nasty bug where notifyListeners wasn't iterating over the
 * whole loop (because the array length in the for() loop was already set) and
 * then clearing the array, so the second listener never got called.
 *
 * The key to this fix was moving "this.waitingForRespose_ = false;" before
 * iterating over the callbacks.
 *
 * Make sure this is fixed.
 */
var testNotifyListenersTransitivelyCallingGetAuthStatusGetCalled = function() {
  var callback1 = function() { auth.getAuthStatus(callback2); },
      callback2 = stubFn();

  auth.waitingForResponse_ = true;
  auth.getAuthStatus(callback1);

  auth.onCheckSuccess({});

  assertTrue('Both callbacks should have been called.', callback2.called);
};


/**
 * auth.logoutFailure_ needs to force checkStatus to make an rpc.execute call
 * but under the wrong circumstances, it wasn't because waitingForResponse_
 * wasn't being set to false prior to the checkStatus call.
 *
 * Make sure that logoutFailure transitively results in a call to rpc.execute.
 */
var testLogoutFailureResultsInRPCExecuteCall = function() {
  auth.logout();

  auth.logoutFailure_();
  assertTrue('RPC.execute should have been called.', rpc.execute.called);
  assertTrue('RPC.execute should have been given an AuthCheckCommand.',
             rpc.execute.args[0] instanceof
             dkpoints.rpc.commands.AuthCheckCommand);
};


var tearDown = function() {
  rpc = menu = statusPanel = auth = null;
};
